/*NOTICE

This software was produced for the U. S. Government
under Contract No. FA8721-09-C-0002, and is
subject to the Rights in Noncommercial Computer Software
and Noncommercial Computer Software Documentation Clause
(DFARS) 252.227-7014 (JUN 1995)

(c) 2009 The MITRE Corporation. All Rights Reserved.
*/

package org.mitre.xmltransform.uml21tospmif4;

// TO DO
// 1. update uml:combinedFragment
// 2. update uml:interactionOccurrence
// 3. update location handling - execution engine support - DONE
// 4. support more node types - Pardo Nodes?
// 5. no behavior executions (no focus of controls...) - or am I going to require them??
// 6. handle local time in a method - add a standard node at the beginning of each method (exec graph) - DONE
// 7. other resource types via SAResource stereotype and GRMAcquire and GRMRelease stereotypes on messages? - gen resources and devices

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import org.mitre.xmltransform.spmif.SpmifOutputDocument;
import org.mitre.xmltransform.spmif.Device;
import org.mitre.xmltransform.transform.Transform;
import org.mitre.xmltransform.uml21.Uml21InputDocument;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Uml21ToSpmif4Transform extends Transform {
	
	@Override
	public void initialize(File pInputFile, File pOutputFile) {
		mInput = new Uml21InputDocument(pInputFile);
		mOutput = new SpmifOutputDocument(pOutputFile);
	}
	
	public int setArguments(String[] pTransformArguments) {
		// any # of arguments is fine
		mTransformArguments = pTransformArguments;
		return SETARGS_NO_ERROR;		
	}

	public String getUsageMessage() {
		// should never get called since any number of transform-specific arguments is fine
		String result = super.getUsageMessage();
		result += " < sequence diagam 1 > [ < sequence diagram 2 > [...]]";
		return result;
	}

	public void transform() {
		if (generateOutput()) {
			System.out.println("Finishing output document.");
			mOutput.finishDocument();
			System.out.println("Writing output document to file.");
			mOutput.writeOutput();
			System.out.println("Transformation complete.");
		}		
	}

	private boolean generateOutput() {
		try {
			String projectName = mInput.getInputValue("/xmi:XMI/uml:Model/@name");
			mOutput.createProjectElement(projectName);
			System.out.println("Creating S-PMIF project " + projectName);
			NodeList interactions = mInput.getInputNodes("//packagedElement[@xmi:type='uml:Interaction']");
			if (interactions == null) {
				System.err.println("** No interactions found.");
				return true;
			}
			Node n;
			System.out.println("Input Model has " + interactions.getLength() + " interactions.");
			for (int i = 0; i < interactions.getLength(); i++) {
				n = interactions.item(i);
				generatePerformanceScenariosForInteraction(n);
			}
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}
		
	private Device getLocation(Node pContext) {
		String type = Device.TYPE_PROCESSING;
		int numProcessors = 1;

		mInput.setVariable("lifelineId", mInput.getInputValue(pContext, "@covered"));
		String lifelinePropertyId = mInput.getInputValue(pContext, "parent::node()/lifeline[@xmi:id=$lifelineId]/@represents");
		mInput.setVariable("lifelinePropertyId", lifelinePropertyId);
		String classId = mInput.getInputValue(pContext, "parent::node()/ownedAttribute[@xmi:type='uml:Property'][@xmi:id=$lifelinePropertyId]/@type");
		mInput.setVariable("classId", classId);

		String executionEngine = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Lifeline=$lifelineId]/@SAExecutionEngine");
		String schedPolicy = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Lifeline=$lifelineId]/@SASchedulingPolicy");
		if (executionEngine == null || executionEngine.length() == 0) {
			executionEngine = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Class=$classId]/@SAExecutionEngine");
			schedPolicy = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Class=$classId]/@SAschedulingPolicy");
		}
		if (executionEngine == null || executionEngine.length() == 0) {
			String resourceId = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAResource[@base_Class=$classId]/@xmi:id");
			if (resourceId != null && resourceId.length() > 0) {
				executionEngine = mInput.getInputValue(pContext, "ancestor::uml:Model/descendant::packagedElement[@xmi:type='uml:Class'][@xmi:id=$classId]/@name"); 
				type = Device.TYPE_PASSIVE;
			} else {
				return null;
			}
		} else {
			if (schedPolicy.equals("RateMonotonic")) {
				schedPolicy = "RM";
			} else if (schedPolicy.equals("FixedPriority")) {
				schedPolicy = "FP";
			} else if (schedPolicy.equals("FIFO")) {
				schedPolicy = "FCFS";
			} else {
				schedPolicy = "PS";
			} // what is PS? What is IS?
			
			// now find the class named <executionEngine>
			mInput.setVariable("executionEngine", executionEngine);
			String engineClassId = mInput.getInputValue(pContext, "ancestor::uml:Model/descendant::packagedElement[@xmi:type='uml:Class'][@name=$executionEngine]/@xmi:id");
			if (engineClassId != null && engineClassId.length() > 0) {
				mInput.setVariable("engineClassId", engineClassId);
				String resources = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAEngine[@base_Class=$engineClassId]/@SAresources");
				if (resources != null) {
					try {
						numProcessors = Integer.parseInt(resources);
					} catch (NumberFormatException e) {
						numProcessors = resources.split(",").length;
					}
				}
			}
		}
		return new Device(executionEngine, type, schedPolicy, numProcessors);
	}
	
	private String interpretTimeInMS(String pTime) {
		// no units assumes time is already in milliseconds
		String units;
		String time = pTime;
		String num;
		int value;
		int commaIndex = time.indexOf(",");
		if (commaIndex != -1) {
			num = time.substring(1, commaIndex);
			try {
				value = Integer.parseInt(num);
			} catch (NumberFormatException e) {
				System.err.println("No valid number found before comma in time expression: " + pTime);
				return null;
			}
			int firstQuoteIndex = time.indexOf("'", commaIndex + 1);
			if (firstQuoteIndex == -1) {
				units = time.trim(); // no quotes is ok
			} else {
				units = time.substring(firstQuoteIndex + 1, time.indexOf("'", firstQuoteIndex + 1));
			}
			if (units.equalsIgnoreCase("m") || units.equalsIgnoreCase("mins")) {
				time = "" + ((float)value*60000.0);
			} else if (units.equalsIgnoreCase("ms")) {
				time = "" + value;
			} else if (units.equalsIgnoreCase("us")) {
				time = "" + ((float)value/1000.0);
			} else if (units.equalsIgnoreCase("ns")) {
				time = "" + ((float)value/1000000.0);
			} else if (units.equalsIgnoreCase("s") || units.equalsIgnoreCase("sec") || units.equalsIgnoreCase("secs")){// seconds
				time = "" + ((float)value*1000.0);
			} else {
				System.err.println("** Unsupported time units (" + units + ") in expression " + pTime);
				return null;
			}
		} else {
			System.err.println("** No units provided for time expression: " + pTime);
			return null;
		}
		return time;
	}
	
	private String determineInterarrivalTime(Node pInteraction, String pMessageId) {
		String interarrivalTime = null;
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message='" + pMessageId + "']/@SAoccurrence")) {
			interarrivalTime = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message='" + pMessageId + "']/@SAoccurrence");
		}
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PAoccurrence")) {
			interarrivalTime = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PAoccurrence");
		}
		if (interarrivalTime != null && interarrivalTime.length() > 0) {
			interarrivalTime = interpretTimeInMS(interarrivalTime);
			if (interarrivalTime == null) {
				System.err.println("Interarrival time expression invalid.");
			}
		} else {
			interarrivalTime = null;
		}
		return interarrivalTime;
	}
	
	private String determinePriority(Node pInteraction, String pMessageId) {
		String priority = null;
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PApriority")) {
			priority = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PApriority");
			if (priority != null && priority.length() > 0) {
				//System.out.println("priority = " + priority);
			} else {
				priority = null;
			}
		}
		return priority;
	}
	
	private boolean hasDuration(String pMessageId, Node pMessageNode, Node pSendEventFragmentNode) {
		String duration = determineDuration(pMessageId, pMessageNode, pSendEventFragmentNode);
		return (duration != null && duration.length() > 0);
	}
	
	private String determineDuration(String pMessageId, Node pMessageNode, Node pSendEventFragmentNode) {
		String duration = mInput.getInputValue(pMessageNode, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Message='" + pMessageId + "']/@RTduration");
		if (duration == null || duration.length() == 0) {
			// no duration on message, try operation
			String sendEventId = mInput.getInputValue(pSendEventFragmentNode, "@event");
			String operationId = mInput.getInputValue("/xmi:XMI/uml:Model//packagedElement[@xmi:type='uml:SendOperationEvent'][@xmi:id='" + sendEventId + "']/@operation");
			if (operationId != null && operationId.length() > 0) {
				duration = mInput.getInputValue(pMessageNode, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Operation='" + operationId + "']/@RTduration");
			}
		}
		if (duration != null && duration.length() > 0) {
			duration = interpretTimeInMS(duration);
			if (duration == null) {
				System.err.println("Invalid time expression for duration.");
			}
		} else {
			duration = null;
		}
		return duration;
	}
		
	private boolean shouldTransform(String pInteractionName) {
		if (mTransformArguments != null && mTransformArguments.length > 0) {
			boolean found = false;
			for (int i = 0; i < mTransformArguments.length; i++) {
				if (pInteractionName.equalsIgnoreCase(mTransformArguments[i])) {
					found = true;
					break;
				}
			}
			if (!found) {
				return false;
			}
		}
		return true;
	}
	
	private boolean VerifyInteractionHasADuration(Node pInteraction)
	{
		boolean result = false;
		NodeList messages = mInput.getInputNodes(pInteraction, "message");
		Node curMessage, sendEventFragment;
		String messageId;
		
		for (int i = 0; i < messages.getLength(); i++) {
			curMessage = messages.item(i);
			messageId = mInput.getInputValue(curMessage, "@xmi:id");
			sendEventFragment = getSendEventFragment(curMessage);
			result = hasDuration(messageId, curMessage, sendEventFragment);
			if (result) break;
		}
	
		return result;
	}

	private void VerifyMessagesHaveOperations(Node pInteraction) {
		NodeList messages = mInput.getInputNodes(pInteraction, "message");
		Node curMessage, sendEventFragment;
		String eventId, operationId, messageName;
		
		for (int i = 0; i < messages.getLength(); i++) {
			curMessage = messages.item(i);
			messageName = mInput.getInputValue(curMessage, "@name");
			sendEventFragment = getSendEventFragment(curMessage);
			eventId = mInput.getInputValue(sendEventFragment, "@event");
			if (eventId != null) {
				operationId = mInput.getInputValue("/xmi:XMI/uml:Model//packagedElement[@xmi:type='uml:SendOperationEvent'][@xmi:id='" + eventId + "']/@operation");				
			} else {
				operationId = null;
			}
			if (operationId == null || operationId.length() == 0) {
				System.out.println("  WARNING. Message '" + messageName + "' does not have an underlying realization.");
			}
		}
	}
		
	private void generatePerformanceScenariosForInteraction(Node pInteraction) {
		// should we generate for this interaction?
		// check based on transform-specific run-time arguments
		String interactionName = mInput.getInputValue(pInteraction, "@name");
		if (!shouldTransform(interactionName)) {
			System.out.println("Not transforming interaction '" + interactionName + "'.");
			return;
		}
		System.out.println("Transforming interaction '" + interactionName + "'.");
		VerifyMessagesHaveOperations(pInteraction);
		if (!VerifyInteractionHasADuration(pInteraction)) {
			System.out.println("  ERROR: Interaction '" + interactionName + "' has no durations on any messages. Please use the RTaction stereotype's RTduration tag to associate a message or its underlying operation with a duration.");
			return;
		}
		
		NodeList fragments = mInput.getInputNodes(pInteraction, "fragment");
		Node fragmentNode = fragments.item(0); // fragments are ordered, messages are not
		String fragId = mInput.getInputValue(fragmentNode, "@xmi:id");
		String firstMessageId = mInput.getInputValue(fragmentNode, "parent::node()/message[@xmi:type='uml:Message'][@sendEvent='" + fragId + "']/@xmi:id");
		if (firstMessageId == null || firstMessageId.length() == 0) {
			// should never get here because interactions always start with a send?
			firstMessageId = mInput.getInputValue(fragmentNode, "parent::node()/message[@xmi:type='uml:Message'][@receiveEvent='" + fragId + "']/@xmi:id");
		}
		String interarrivalTime = determineInterarrivalTime(pInteraction, firstMessageId);
		String priority = determinePriority(pInteraction, firstMessageId);

		Node receiveEventFragment = getReceiveEventFragment(mInput.getInputNode(pInteraction, "message[@xmi:id='" + firstMessageId + "']"));
		String executionLifelineId = mInput.getInputValue(receiveEventFragment, "@covered");
		String execGraphName = buildEventOccurrenceNodeName(receiveEventFragment);
		Device location = getLocation(receiveEventFragment);
		
		System.out.println("  Creating performance scenario '" + interactionName + "' (" + (interarrivalTime == null ? "No interarrival time" : "Interarrival time = " + interarrivalTime + " ms") + ", Priority = " + priority + ").");
		
		// create perf scenario and main exec graph
		Element perfScenarioElement = mOutput.createPerformanceScenario(interactionName, mInput.getFileName(), interarrivalTime, "ms", priority);

		System.out.println("  Creating main execution graph '" + execGraphName + "'.");
		Element executionGraphElement = createExecutionGraph(
				perfScenarioElement, execGraphName, true);
				
		NodeHierarchy hierarchy = new NodeHierarchy(perfScenarioElement, executionGraphElement, interactionName, null, location, executionLifelineId);
		mOutput.registerLocation(location);
		
		generateExecutionGraph(fragments, hierarchy);
	}	
	
	// create an execution step per message
	// 
	private void generateExecutionGraph(NodeList pFragments, NodeHierarchy pHierarchy) {
		Node messageNode, fragmentNode;
		Node receiveEventFragment, sendEventFragment;
		String sendLifelineId, receiveLifelineId, id, fragId, sendEventId, operationId;
		Device endNodeLocation;
		String duration;
		String name, eventType;
		HierarchalNode hnode;
		boolean async;
		Map<String, Node> completedMessages = new HashMap<String, Node>();
		
		// messages to do not appear in the order they are executed in the sequence diagram.
		// they appear in the order they are drawn
		// Rhapsody has its own profile that provides sequence numbers for each message
		// but that is Rhapsody specific.
		// Fragments are required to be in order per UML2 spec.
		
		// skip the first 2 fragments as representing the send and receive event for the first message
		for (int fragIndex = 2; fragIndex < pFragments.getLength(); fragIndex++) {
			fragmentNode = pFragments.item(fragIndex);
			fragId = mInput.getInputValue(fragmentNode, "@xmi:id");
			messageNode = mInput.getInputNode(fragmentNode, "parent::node()/message[@xmi:type='uml:Message'][@receiveEvent='" + fragId + "']");
			if (messageNode == null) {
				messageNode = mInput.getInputNode(fragmentNode, "parent::node()/message[@xmi:type='uml:Message'][@sendEvent='" + fragId + "']");
			}
			if (messageNode != null) {
				id = mInput.getInputValue(messageNode, "@xmi:id");
				if (!completedMessages.containsKey(id)) {
					completedMessages.put(id, messageNode);
					receiveEventFragment = getReceiveEventFragment(messageNode);
					receiveLifelineId = mInput.getInputValue(receiveEventFragment, "@covered");
					sendEventFragment = getSendEventFragment(messageNode);
					sendLifelineId = mInput.getInputValue(sendEventFragment, "@covered");

					// check for async
					sendEventId = mInput.getInputValue(sendEventFragment, "@event");
					eventType = mInput.getInputValue(sendEventFragment, "ancestor::uml:Model/descendant::packagedElement[@xmi:id='" + sendEventId + "']/@xmi:type");
					async = (eventType.equals("uml:SignalEvent"));
					
					// duration
					duration = determineDuration(id, messageNode, sendEventFragment);

					name = buildEventOccurrenceNodeName(receiveEventFragment);

					// get location
					endNodeLocation = getLocation(receiveEventFragment);
					mOutput.registerLocation(endNodeLocation);
		
					System.out.println("    Creating Node '" + name + "' (" + (duration == null ? "No duration" : "Duration = " + duration + "ms") + ", Asynchronous = " + async + ", Location = " + ((endNodeLocation == null) ? "NONE" : endNodeLocation.getName()) + ")");
					hnode = new HierarchalNode(id, name, endNodeLocation, receiveLifelineId, sendLifelineId, duration, messageNode, pHierarchy, async);
					pHierarchy.addNode(hnode);
				}
			}
		}
		
		// start node
		if (pHierarchy.getNumberOfRootNodes() > 0) {
			BaseNode startNode = pHierarchy.getRootNode(0);
			pHierarchy.getExecutionGraphElement().setAttribute("StartNode", startNode.getOutermostSpmifId());
			System.out.println("    Start Node = " + startNode.getName());
		} else {
			pHierarchy.getExecutionGraphElement().setAttribute("StartNode", "");
		}
		
		System.out.println("  Generating S-PMIF from node hierarchy.");
		
		// nodes
		BaseNode node;
		for (int i = 0; i < pHierarchy.getNumberOfRootNodes(); i++) {
			node = pHierarchy.getRootNode(i);
			node.generateStep(pHierarchy.getExecutionGraphElement(), mOutput);
		}
		
		// arcs
		String fromNode = null;
		for (int i = 0; i < pHierarchy.getNumberOfRootNodes(); i++) {
			node = pHierarchy.getRootNode(i);
			fromNode = node.generateArcs(pHierarchy.getExecutionGraphElement(), fromNode, mOutput);
		}
		//return index;
	}
	
	protected Element createExecutionGraph(Element pParent, String pName, boolean pIsMain) {
		String id = pName;
		return mOutput.createExecutionGraph(pParent, id, pName, pIsMain);
	}

	protected String buildEventOccurrenceNodeName(Node pEventOccurrence) {
		StringBuffer result = new StringBuffer();

		mInput.setVariable("fragId", mInput.getInputValue(pEventOccurrence, "@xmi:id"));
		mInput.setVariable("modelId", mInput.getInputValue("/xmi:XMI/uml:Model/@xmi:id"));
		String messageName = mInput.getInputValue(pEventOccurrence, "parent::node()/message[@receiveEvent=$fragId]/@name");
		mInput.setVariable("lifelineId", mInput.getInputValue(pEventOccurrence, "@covered"));
		String lifelineInstanceName = mInput.getInputValue(pEventOccurrence, "parent::node()/lifeline[@xmi:id=$lifelineId]/@name");
		mInput.setVariable("lifelinePropId", mInput.getInputValue(pEventOccurrence, "parent::node()/lifeline[@xmi:id=$lifelineId]/@represents"));
		mInput.setVariable("lifelineClassId", mInput.getInputValue(pEventOccurrence, "parent::node()/ownedAttribute[@xmi:id=$lifelinePropId]/@type"));
		String lifelineClassName = mInput.getInputValue(pEventOccurrence, "ancestor::uml:Model/descendant::packagedElement[@xmi:type='uml:Class'][@xmi:id=$lifelineClassId]/@name");
		String name = lifelineClassName;
		if (lifelineClassName.length() == 0) {
			name = lifelineInstanceName;
		}
		if (messageName.length() == 0) {
			//TO DO: add stereotype support - not used by E-10A
/*			if (mInput.evaluateBooleanInputExpression(pEventOccurrence, "parent::node()/message[@xmi:id=$messageId]/uml:ModelElement.stereotype")) {
				mInput.setVariable("stereotypeId", mInput.getInputValue(pEventOccurrence, "ancestor::uml:Interaction/uml:Interaction.message/uml:Message[@xmi:id=$messageId]/uml:ModelElement.stereotype/uml:Stereotype/@xmi:idref"));
				String stereotypeName = mInput.getInputValue(pEventOccurrence, "/XMI/XMI.content/uml:Model[@xmi:id=$modelId]/uml:Namespace.ownedElement/uml:Stereotype[@xmi:id=$stereotypeId]/@name");
				result.append(stereotypeName);
			} else {
				result.append("anon");
			}*/
			result.append("_");
			result.append(name);
		} else {
			result.append(messageName);
			//TO DO: argument value support - not used by E-10A
			/*NodeList messages = mInput.getInputNodes(pEventOccurrence, "ancestor::uml:Interaction/uml:Interaction.message/uml:Message[@xmi:id=$messageId]/uml:Message.argument/uml:OpaqueExpression[string-length(@body) > 0]");
			Node n;
			for (int i = 0; i < messages.getLength(); i++) {
				n = messages.item(i);
				result.append("_");
				result.append(mInput.getInputValue(n, "@body"));
			}*/
			result.append("_");
			result.append(name);
		}
		//let's put a the node number of the uml:EventOccurrence within the context of the entire XMI 
		result.append("_");
		NodeList eventOccurrences = mInput.getInputNodes(pEventOccurrence, "parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification']");
		Node eo;
		String xmiId;
		String nodeId = mInput.getInputValue(pEventOccurrence, "@xmi:id");
		for (int i = 0; i < eventOccurrences.getLength(); i++) {
			eo = eventOccurrences.item(i);
			xmiId = mInput.getInputValue(eo, "@xmi:id");
			if (xmiId.equals(nodeId)) {
				result.append((i+1));
				break;
			}
		}
		return result.toString();
	}

	private Node getReceiveEventFragment(Node pMessage) {
		Node result = null;
		String endNodeId = mInput.getInputValue(pMessage, "@receiveEvent");
		if (endNodeId != null && endNodeId.length() > 0) {
			result = mInput.getInputNode(pMessage, 
					"parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + endNodeId + "']");
		}
		return result;
	}
	
	private Node getSendEventFragment(Node pMessage) {
		Node result = null;
		String startNodeId = mInput.getInputValue(pMessage, "@sendEvent");
		if (startNodeId != null && startNodeId.length() > 0) {
			result = mInput.getInputNode(pMessage, 
					"parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + startNodeId + "']");
		}
		return result;		
	}

	private String[] mTransformArguments;
	private Uml21InputDocument mInput;
	private SpmifOutputDocument mOutput;
}
